iT邦幫忙

2017 iT 邦幫忙鐵人賽
DAY 14
0
Modern Web

寫React的那些事系列 第 14

React Day14 - TODOS Demo(2)

  • 分享至 

  • xImage
  •  

今天要做傳入todos,並且可以修改todo item,當修改的時候原本todo item會變成修改的input,裡面有一些小小tips喔!先執行npm run dev,然後一步一步做下去,就可以直接從瀏覽器上看到變化。

Step1


app.js

首先,我們要在app.js新增一個陣列todos:

const todos = [
  {
    task: 'Install packages',
    isCompleted: false
  },
  {
    task: 'Add webpack.config.js',
    isCompleted: false
  },
  {
    task: 'Break UI into components',
    isCompleted: false
  }
];

這個新增的todos還沒有傳入,class App裡面,所以我們要設定App的state初始值,還記得前面提到state初始化,我們寫在constructor中,並且需要super一下。

  • 這邊有一個地方是ES6的寫法,如果object的key名稱和value變數一樣,可以簡化{ todo: todo }寫成{ todo }即可。可以參考ES6 Property Shorthand
constructor(props) {
  super(props);

  this.state = {
    todos
  };
}

然後,App已經有todos存在state裡,我們必須把它傳給TodoList去render items:

<TodoList todos={this.state.todos} />

TodoList.js

對TodoList來說,它會接收到props,可以在render function中, coonsole.log(this.props) 看看,會看到有傳入一個object,裡面有一個todos,長度為3的陣列。

而我們這邊要做的就是render items,建立另一個_renderItems function,通常非內建function可以做一點區別會比較好閱讀,我這邊是寫在所有內建function最下面(通常是render後面),並且名稱加上底線,可以建立自己的習慣就好。

  • 除了React內建的function,自己建立的function必須用bind把this指定給_renderItems,才會指到class本身,這是React裡面很重要的觀念。(某些情況也可以用arrow function,總之要注意this的使用,否則會抓不到state/props)
  • 把todos的每個index,用forEach設定給TodoItem。
  • forEach裡面使用ES6的Arrow Function
  • 如果要用陣列的方式列出element,React規定必須加上key(index),若沒有加,在console會看到warning訊息。而子元件沒有辦法抓到這個props key,所以我把之後會用到的idx也傳入。
// constructor習慣上寫在class裡面的第一個function位置
constructor(props) {
  super(props);
  this._renderItems = this._renderItems.bind(this);
}

// ...

// 在class最後面,render function之後加
_renderItems() {
  const todos = this.props.todos;

  let list = [];
  todos.forEach((todo, idx) => {
      list.push(<TodoItem key={idx} idx={idx} todo={todo} />);
  });
  return list;
}

TodoItem.js

這樣我們在TodoItem就可以收到props,這時的props只是單一task的內容

render() {
  const todo = this.props.todo;
  return (
    <tr>
        <td>{todo.task}</td>
        <td>
            <button>Edit</button>
            <button>Delete</button>
        </td>
    </tr>
  );
}

到目前的步驟,就可以看到一開始我們設定的todos陣列,每一個項目都正確的顯示在畫面中囉!

Step2


再來,我們要編輯單一task的內容。

TodoItem.js

因為我們要編輯task的時候,希望畫面改變成輸入框與加上儲存按鈕,畫面的變化都是由props或state來控制,這邊算是改變item的內部狀況,所以我們用一個內部的state變數來控制。並設定bind待會會用到的兩個新的function。

constructor(props) {
  super(props);
  this.state = {
    isEditing: false
  };
  this._onEditClick = this._onEditClick.bind(this);
  this._onCancelClick = this._onCancelClick.bind(this);
}

當isEditing為true的時候,顯示編輯的畫面:

  • 這邊object可以直接指定他的key內容,也是使用ES6語法,參考ES6 Destructuring assignment
  • 編輯畫面時,task顯示在input中,因為input可以讓user修改,在React中表單的欄位也一些特別設定,如果我們要給他預設值必須使用defaultValue。
  • button設定事件也是用camelCase,EX: onClick
render() {
  const { todo, idx } = this.props;
  if (this.state.isEditing) {
    return (
      <tr>
        <td><input type="text" data-idx={idx} defaultValue={todo.task} /></td>
        <td>
          <button>Save</button>
          <button onClick={this._onCancelClick}>Cancel</button>
        </td>
      </tr>
    );
  }

  return (
    <tr>
      <td>{todo.task}</td>
      <td>
        <button onClick={this._onEditClick}>Edit</button>
        <button>Delete</button>
      </td>
    </tr>
  );
}

再來,當click Edit/Cancel時,切換isEditing的狀態,讓畫面依照state來render不同的狀態:

_onEditClick() {
  this.setState({ isEditing: true });
}

_onCancelClick() {
  this.setState({ isEditing: false });
}

目前做到這邊,已經可以顯示編輯畫面囉!用state來控制元件本身的狀態,是不是有點fu了呢~

Step3


最後,我們要來儲存user編輯的input內容,因為要改動的是props的內容,是整個資料內容,我們不能在component中改props,所以必須從最上層app.js改動,然後把改內容的function當成另一個props傳入。

  • 在constructor時,設定bind。
  • 把saveTask,傳入TodoList,讓子元件可以呼叫這個function。
  • 建立_saveTask,並傳入兩個參數,第一個是要改變的todos索引值,第二個是input數入的值,這邊有一個點是,我們不能直接對state做修改,但是Javascript的array和object都是存reference,所以我們必須把array和object做處理。這邊使用ES6 Spread Operator,快速的copy成另一個array,可是裡面的object還是指向同一個位置,所以要再使用Object.assign,把object也複製一個新的,並把要改變的值傳入。這邊的reference也許不一定要特別複製,因為React在比對props和state時是shallow comparison,但個人習慣保持prev state和next state的乾淨,這對之後我會介紹到Redux也有幫助喔!

app.js

class App extends Component {
  constructor(props) {
    super(props);

    this.state = {
      todos
    };
    this._saveTask = this._saveTask.bind(this);
  }

  render() {
    return (
      <div>
        <h1>React Todo List</h1>
        <TodoAdd />
        <TodoList todos={this.state.todos} saveTask={this._saveTask} />
      </div>
    );
  }

  _saveTask(idx, val) {
    // copy array
    let newTodos = [...this.state.todos];
    // copy object
    newTodos[idx] = Object.assign({}, newTodos[idx], { task: val });
    this.setState({ todos: newTodos });
  }
}

TodoList.js

TodoList只需要把props的saveTask,再傳給TodoItem:

_renderItems() {
  const { todos, saveTask } = this.props;

  let list = [];
  todos.forEach((todo, idx) => {
    list.push(<TodoItem key={idx} idx={idx} todo={todo} saveTask={saveTask} />);
  });
  return list;
}

TodoItem.js

如同_onEditClick和_onCancelClick,我們要加一個_onSaveClick的function,並且先在constructor設定bind。

this._onSaveClick = this._onSaveClick.bind(this);

然後,給user填寫的input,我們必須給它一個ref,這是React裡面指定元件的名稱,我們可以使用this.refs.editInput得到這個元件。並且設定data-idx,把之前得到這個todo的索引值設定給input。並在Save button設定onClick。

render() {
  const { todo, idx } = this.props;
  if (this.state.isEditing) {
    return (
      <tr>
        <td><input type="text" data-idx={idx} defaultValue={todo.task} ref="editInput" /></td>
        <td>
          <button onClick={this._onSaveClick}>Save</button>
          <button onClick={this._onCancelClick}>Cancel</button>
        </td>
      </tr>
    );
  }

  return (
    <tr>
      <td>{todo.task}</td>
      <td>
        <button onClick={this._onEditClick}>Edit</button>
        <button>Delete</button>
      </td>
    </tr>
  );
}

最後,新增_onSaveClick function,取得this.refs.editInput的data-idx屬性值與value,並傳給props的saveTask function,在getAttribute後用一個 + 把字串轉成數值。然後記得把isEditing state設定回false,這樣就完成儲存的動作。

_onSaveClick() {
  const input = this.refs.editInput;
  this.props.saveTask(+input.getAttribute('data-idx'), input.value);
  this.setState({ isEditing: false });
}

今天的檔案已經放在Git上,到目前為止已經很清楚可以了解state和props是如何串起React元件的資料流了吧!


上一篇
React Day13 - TODOS Demo(1)
下一篇
React Day15 - TODOS Demo(3)
系列文
寫React的那些事31
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言